//
// Created by tang on 23-3-11.
//

#include "plugin_mgr.hpp"
#include <filesystem>
#include <json/json.hpp>
#include "platform/dynamic_library.hpp"
#include "utils/io_tools.hpp"
#include "utils/path_tools.hpp"
#include "core/log.hpp"

struct PluginManagerConfig {
    std::vector<std::string> paths;
    std::vector<std::string> blacklist;
    std::vector<std::string> required;

    NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT (PluginManagerConfig, paths, blacklist, required);
};

bool PluginManager::init ()
{
    using namespace nlohmann;

    std::string cfg_path = "plugin_manager.json"_AP;
    std::string content  = ReadTextFile (cfg_path);

    auto                obj = json::parse (content);
    PluginManagerConfig cfg;
    obj.get_to (cfg);

    auto plugins = find_all_plugins (cfg.paths);

    // load all plugins
    for (std::filesystem::path plugin_file: plugins)
    {
        std::string name   = plugin_file.stem ().string ();
        LoaderPtr   loader = std::make_shared<PluginLoader> (plugin_file.string ());
        if (loader->try_load()) {
            loadable_plugins.emplace (std::pair{name, loader});
        } else {
            L_WARN ("Bad plugin '%s'", name.c_str());
        }
    }

    // load required plugins
    std::set<std::string> required{begin(cfg.required), end(cfg.required)};
    std::set<std::string> blocked{begin(cfg.blacklist), end(cfg.blacklist)};

    for (auto const &x: loadable_plugins) {
        const bool is_required = required.contains (x.first);
        const bool is_blocked = blocked.contains (x.first);
        if (is_required && !is_blocked)
        {
            bool ok = x.second->active();
            if (ok) {
                required.erase (x.first);
                L_DEBUG ("Required plugin '%s' automatic load success!", x.first.c_str());
            } else {
                L_DEBUG ("Required plugin '%s' automatic load failed!", x.first.c_str());
            }
        } else if (is_required) {
            L_WARN ("Required plugin '%s' in blacklist!!", x.first.c_str());
        } else if (!is_blocked) {
            bool ok = x.second->active();
            if (ok) {
                required.erase (x.first);
                L_DEBUG ("Plugin '%s' automatic load success!", x.first.c_str());
            }
        } else { // is_blocked
            L_DEBUG ("Plugin '%s' blocked, ignore", x.first.c_str());
        }
    }

    if (!required.empty()) {
        L_WARN ("Required plugin(s) not all loaded!!");
    }

    return true;
}

void PluginManager::clean () {
    for (auto &x: loadable_plugins) {
        if (x.second->is_loaded())
            x.second->deactivate();
    }
    loadable_plugins.clear();
}

void PluginManager::on_tick(f32 delta, f32 time) {
    for (auto const &x: loadable_plugins) {
        if (x.second->is_loaded()) {
            x.second->plugin_inst->tick(delta, time);
        }
    }
}

bool PluginManager::active (std::string_view plugin_name)
{
    auto it = loadable_plugins.find (std::string {plugin_name});
    if (it != end (loadable_plugins))
    {
        return it->second->active ();
    }
    L_WARN ("Plugin {} not found!", plugin_name.data ());
    return false;
}

bool PluginManager::deactivate (std::string_view plugin_name)
{
    auto it = loadable_plugins.find (std::string {plugin_name});
    if (it != end (loadable_plugins))
    {
        return it->second->deactivate ();
    }
    L_WARN ("Plugin {} not found!", plugin_name.data ());
    return false;
}

std::vector<std::string> PluginManager::find_all_plugins (const std::vector<std::string> &paths)
{
    namespace fs = std::filesystem;
    std::vector<std::string> result;

    for (auto const &dir: paths)
    {
        fs::path path;
        if (fs::path(dir).is_relative() || fs::is_directory (dir)) {
            path = fs::path{GetAppBinaryDirPath()}/dir;
        } else {
            path = dir;
        }
        for (auto const &file: fs::directory_iterator {path})
        {
            if (file.is_regular_file ())
            {
                if (file.path ().extension ().string () == ".plugin")
                {
                    result.emplace_back (canonical (absolute (file.path ())).string ());
                    L_DEBUG ("Got Plugin: %s", result.back ().c_str ());
                }
            }
        }
    }

    return result;
}

bool PluginManager::PluginLoader::try_load ()
{
    library = new DynamicLibrary (full_path);
    return (bool) *library;
}
bool PluginManager::PluginLoader::active ()
{
    if (plugin_inst == nullptr)
    {
        using entry_point = IPlugin *(*) ();
        auto func         = library->resolve<entry_point> ("create_plugin");
        if (func)
        {
            plugin_inst = func ();
            if (plugin_inst && plugin_inst->initialize ())
            {
                L_DEBUG ("Plugin '%s' Loaded.", plugin_inst->name());
            }
            else
            {
                delete plugin_inst;
                plugin_inst = nullptr;
            }
        }
        else
        {
            L_ERROR ("EntryPoint function 'create_plugin' not found in file %s", full_path.c_str ());
        }
    }
    return plugin_inst != nullptr;
}
void PluginManager::PluginLoader::shutdown ()
{
    if (is_loaded())
        deactivate ();
    delete library;
    library = nullptr;
}
bool PluginManager::PluginLoader::deactivate ()
{
    if (plugin_inst)
    {
        plugin_inst->cleanup ();
        delete plugin_inst;
        plugin_inst = nullptr;
        return true;
    }
    L_WARN ("Plugin Not Activate!!");
    return false;
}
PluginManager::PluginLoader::PluginLoader (std::string fp) : full_path {std::move (fp)}, plugin_inst {nullptr}, library {nullptr} {}
PluginManager::PluginLoader::~PluginLoader ()
{
    shutdown ();
}
